﻿namespace Legend.Core.Tests.Web.HtmlTextWriterExtensions
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Web.UI;
    using FakeItEasy;
    using Legend.Web;
    using NUnit.Framework;

    public class HtmlTextWriterTestBase
    {
        protected HtmlTextWriter writer;

        [SetUp]
        public void SetUp()
        {
            this.OnSetup();
        }

        protected virtual void OnSetup()
        {
            this.writer = A.Fake<HtmlTextWriter>();
        }
    }

    public class SyntaxTests
    {
        public void test_full_syntax()
        {
            var ages = new Dictionary<string, int>() 
            {
                {"John Doe", 30},
                {"Jane Doe", 25}
            };

            string result = @"
            <html>
                <head>
                    <title>Hello world</title>
                    <link type='text/css' rel='stylesheet' href='http://foo.com/style.css' />
                </head>
                <body class='Foo'>
                    <h1>Welcome</h1>
                    <select id='numberSelect' name='numberSelect'>
                        <option>0</option>
                        <option selected='true'>1</option>
                        <option>2</option>
                    </select>
                    <span>John Doe is 30 years old.</span>
                    <span>Jane Doe is 25 years old.</span>
                </body>
            </html>";

            var sw = new StringWriter();

            using (var writer = new HtmlTextWriter(sw))
            {

                writer
                    .Html()
                        .Tag(HtmlTextWriterTag.Head)
                            .Tag(HtmlTextWriterTag.Title)
                                .Text("Hello world")
                            .EndTag()
                            .Tag(HtmlTextWriterTag.Link, a => a
                                [HtmlTextWriterAttribute.Type, "text/css"]
                                [HtmlTextWriterAttribute.Rel, "stylesheet"]
                                [HtmlTextWriterAttribute.Href, "http://foo.com/style.css"]
                             )
                            .EndTag()
                        .EndTag()
                        .Body(a => a.Class("Foo"))
                            .Tag(HtmlTextWriterTag.H1).Text("Welcome").EndTag()
                            .Tag(HtmlTextWriterTag.Select, a => a.Id("numberSelect").Name("numberSelect"))
                                .Repeat(3, (number, w) => w
                                    .Tag(HtmlTextWriterTag.Option)
                                        .Text(number.ToString())
                                    .EndTag()
                                )
                            .EndTag()
                            .Bind(ages, (nameAndAge, index, w) => w
                                .Span()
                                    .Text(string.Format("{0} is {1} years old.", nameAndAge.Key, nameAndAge.Value))
                                .EndTag()
                            )
                        .EndTag()
                    .EndTag();
            }

            Debug.Write(sw.GetStringBuilder().ToString());
        }
    }

    [TestFixture]
    public class Tag
        : HtmlTextWriterTestBase
    {
        [Test]
        public void returns_writer()
        {
            HtmlTextWriter returned = this.writer.Tag(HtmlTextWriterTag.Div);
            Assert.AreSame(this.writer, returned);
        }

        [Test]
        public void renders_begin_tag()
        {
            this.writer.Tag(HtmlTextWriterTag.Div);
            A.CallTo(() => this.writer.RenderBeginTag(HtmlTextWriterTag.Div)).MustHaveHappened();
        }
    }

    [TestFixture]
    public class Tag_with_attributes
        : HtmlTextWriterTestBase
    {
        [Test, ExpectedException(typeof(ArgumentNullException))]
        public void throws_when_appender_is_null()
        {
            this.writer.Tag(HtmlTextWriterTag.Div, (Func<HtmlAttributeManager, HtmlAttributeManager>)null);
        }

        [Test]
        public void renders_begin_tag()
        {
            this.writer.Tag(HtmlTextWriterTag.Div, a => a[System.Web.UI.HtmlTextWriterAttribute.Id, "id"]);
            A.CallTo(() => this.writer.RenderBeginTag(HtmlTextWriterTag.Div)).MustHaveHappened();
        }

        [Test]
        public void returns_writer()
        {
            HtmlTextWriter returned = this.writer.Tag(HtmlTextWriterTag.Div, a => a[HtmlTextWriterAttribute.Id, "id"]);
            Assert.AreSame(this.writer, returned);
        }

        [Test]
        public void passes_correct_HtmlAttributeManager_into_delegate()
        { 
            HtmlAttributeManager passedInstanced = null;

            this.writer.Tag(HtmlTextWriterTag.Div, e => passedInstanced = e);

            Assert.AreSame(this.writer, passedInstanced.Writer);
        }
    }

    [TestFixture]
    public class EndTag
        : HtmlTextWriterTestBase
    {
        [Test]
        public void calls_RenderEndTag_when_called()
        {
            this.writer.EndTag();
            A.CallTo(() => this.writer.RenderEndTag()).MustHaveHappened();
        }

        [Test]
        public void calls_RenderEndTag_when_called_with_tag()
        {
            this.writer.EndTag(HtmlTextWriterTag.Div);
            A.CallTo(() => this.writer.RenderEndTag()).MustHaveHappened();
        }

        [Test]
        public void returns_writer()
        {
            HtmlTextWriter returned = this.writer.EndTag();
            Assert.AreSame(this.writer, returned);
        }

        [Test]
        public void returns_writer_when_called_with_tag()
        {
            HtmlTextWriter returned = this.writer.EndTag(HtmlTextWriterTag.Div);
            Assert.AreSame(this.writer, returned);
        }
    }

    [TestFixture]
    public class Text
        : HtmlTextWriterTestBase
    {
        [Test]
        public void writes_text()
        {
            this.writer.Text("test");
            A.CallTo(() => this.writer.Write("test")).MustHaveHappened();
        }

        [Test]
        public void returns_writer()
        {
            HtmlTextWriter returned = this.writer.Text("test");
            Assert.AreSame(this.writer, returned);
        }
    }

    [TestFixture]
    public class Text_object_overload
        : HtmlTextWriterTestBase 
    {
        [Test]
        public void writes_nothing_for_null_value()
        {
            this.writer.Text(null as object);
            A.CallTo(() => this.writer.Write((string)null)).MustNotHaveHappened();
        }

        [Test]
        public void returns_writer()
        {
            HtmlTextWriter returned = this.writer.Text(null as object);
            Assert.AreSame(this.writer, returned);
        }

        [Test]
        public void uses_IFormattables_ToString_if_implemented_by_object()
        {
            var formattable = A.Fake<IFormattable>();
            A.CallTo(() => formattable.ToString(null, null)).Returns("a");

            this.writer.Text((object)formattable);

            A.CallTo(() => this.writer.Write("a")).MustHaveHappened();
        }

        [Test]
        public void uses_ToString_for_non_IFormattable_object()
        {
            this.writer.Text(new Foo());

            A.CallTo(() => this.writer.Write("Foo")).MustHaveHappened();
        }

        private class Foo
        {
            public override string ToString()
            {
                return "Foo";
            }
        }
    }

    [TestFixture]
    public class Text_htmlEncode_overload
        : HtmlTextWriterTestBase
    {
        [Test]
        public void does_not_encode_when_called_with_false()
        {
            this.writer.Text("&", false);
            A.CallTo(() => this.writer.Write("&")).MustHaveHappened();
        }

        [Test]
        public void encodes_when_called_with_true()
        {
            this.writer.Text("&", true);
            A.CallTo(() => this.writer.Write("&amp;")).MustHaveHappened();
        }

        [Test]
        public void returns_writer()
        {
            HtmlTextWriter returned = this.writer.Text("test", false);
            Assert.AreSame(this.writer, returned);
        }
    }

    [TestFixture]
    public class Bind
        : HtmlTextWriterTestBase
    {
        [Test, ExpectedException(typeof(ArgumentNullException))]
        public void throws_when_collection_is_null()
        {
            this.writer.Bind((IEnumerable<object>)null,
                (item, index, w) => w);
        }

        [Test, ExpectedException(typeof(ArgumentNullException))]
        public void throws_when_binding_function_is_null()
        {
            this.writer.Bind(new string[] { "", "" },
                null);
        }

        [Test]
        public void calls_binder_for_each_item()
        { 
            this.writer.Bind(new string[] {"a", "b"},
                (item, index, w) => w.Text(item));

            A.CallTo(() => writer.Write("a")).MustHaveHappened();
            A.CallTo(() => writer.Write("b")).MustHaveHappened();
        }

        [Test]
        public void increases_index_for_each_item()
        {
            this.writer.Bind(new string[] { "a", "b" },
                (item, index, w) => w.Text(index.ToString()));

            A.CallTo(() => writer.Write("0")).MustHaveHappened();
            A.CallTo(() => writer.Write("1")).MustHaveHappened();
        }

        [Test]
        public void returns_writer()
        {
            HtmlTextWriter returned = this.writer.Bind("ab",
                (item, index, w) => w.Text(item.ToString()));
            Assert.AreSame(this.writer, returned);
        }
    }

    [TestFixture]
    public class Repeat
        : HtmlTextWriterTestBase
    {
        [Test, ExpectedException(typeof(ArgumentOutOfRangeException))]
        public void throws_when_numberOfTimes_is_a_negative_number()
        {
            this.writer.Repeat(-1, (index, w) => w.Text(index.ToString()));
        }

        [Test, ExpectedException(typeof(ArgumentNullException))]
        public void throws_when_binder_is_null()
        {
            this.writer.Repeat(0, null);
        }

        [Test]
        public void calls_binder_correct_number_of_times()
        {
            this.writer.Repeat(2,
                (index, w) => w.Text(index.ToString()));

            A.CallTo(() => this.writer.Write("1")).MustHaveHappened();
            A.CallTo(() => this.writer.Write("2")).MustHaveHappened();
            A.CallTo(() => this.writer.Write("3")).MustNotHaveHappened();
        }

        [Test]
        public void returns_writer()
        {
            HtmlTextWriter returned = this.writer.Repeat(1, (index, w) => w.Text("a"));
            Assert.AreSame(this.writer, returned);
        }
    }

    /// <summary>
    /// Inherit this to create a test for a extension methods for a named
    /// tag. (Like the Div-overloads that creates div tags.
    /// </summary>
    public abstract class NamedTagTestBase
        : HtmlTextWriterTestBase
    {
        [Test]
        public void renders_tag()
        {
            this.CallTagWithNoAttributes();
            A.CallTo(() => this.writer.RenderBeginTag(this.Tag)).MustHaveHappened();
        }

        [Test]
        public void renders_tag_and_attributes()
        {
            this.CallTagWithAttributes(e => e.Id("id"));

            A.CallTo(() => this.writer.AddAttribute(HtmlTextWriterAttribute.Id, "id")).MustHaveHappened();
            A.CallTo(() => this.writer.RenderBeginTag(this.Tag)).MustHaveHappened();
        }

        [Test, ExpectedException(typeof(ArgumentNullException))]
        public void throws_when_attributer_is_null()
        {
            this.CallTagWithAttributes(null);
        }

        /// <summary>
        /// Make a call to the tested overload that
        /// takes no attributes.
        /// </summary>
        protected abstract void CallTagWithNoAttributes();

        /// <summary>
        /// Make a call to the tested overload that takes attributes.
        /// </summary>
        /// <param name="attributes">The attributes the test passes.</param>
        protected abstract void CallTagWithAttributes(Func<HtmlAttributeManager, HtmlAttributeManager> attributes);

        /// <summary>
        /// The type of element the tested methods are supposed to render.
        /// </summary>
        protected abstract HtmlTextWriterTag Tag { get; }
    }

    [TestFixture]
    public class Div
        : NamedTagTestBase
    {

        protected override void CallTagWithNoAttributes()
        {
            this.writer.Div();
        }

        protected override void CallTagWithAttributes(Func<HtmlAttributeManager, HtmlAttributeManager> attributes)
        {
            this.writer.Div(attributes);
        }

        protected override HtmlTextWriterTag Tag
        {
            get
            {
                return HtmlTextWriterTag.Div;
            }
        }
    }

    [TestFixture]
    public class Body : NamedTagTestBase
    {
        protected override void CallTagWithNoAttributes()
        {
            this.writer.Body();
        }

        protected override void CallTagWithAttributes(Func<HtmlAttributeManager, HtmlAttributeManager> attributes)
        {
            this.writer.Body(attributes);
        }

        protected override HtmlTextWriterTag Tag
        {
            get { return HtmlTextWriterTag.Body; }
        }
    }

    [TestFixture]
    public class Html : NamedTagTestBase
    {

        protected override void CallTagWithNoAttributes()
        {
            this.writer.Html();
        }

        protected override void CallTagWithAttributes(Func<HtmlAttributeManager, HtmlAttributeManager> attributes)
        {
            this.writer.Html(attributes);
        }

        protected override HtmlTextWriterTag Tag
        {
            get { return HtmlTextWriterTag.Html; }
        }
    }

    [TestFixture]
    public class Span : NamedTagTestBase
    {

        protected override void CallTagWithNoAttributes()
        {
            this.writer.Span();
        }

        protected override void CallTagWithAttributes(Func<HtmlAttributeManager, HtmlAttributeManager> attributes)
        {
            this.writer.Span(attributes);
        }

        protected override HtmlTextWriterTag Tag
        {
            get { return HtmlTextWriterTag.Span; }
        }
    }

    [TestFixture]
    public class ATEsts
        : HtmlTextWriterTestBase 
    {
        [Test]
        public void renders_tag()
        {
            this.writer.Href("http://foo.com/", "Some title");
            A.CallTo(() => this.writer.RenderBeginTag(HtmlTextWriterTag.A)).MustHaveHappened();
        }

        [Test]
        public void adds_href_attribute()
        {
            this.writer.Href("http://foo.com/", "Some title");
            A.CallTo(() => this.writer.AddAttribute(HtmlTextWriterAttribute.Href, "http://foo.com/")).MustHaveHappened();
        }

        [Test, ExpectedException(typeof(ArgumentNullException))]
        public void throws_when_url_is_null()
        {
            this.writer.Href(null, "Some title");
        }

        [Test, ExpectedException(typeof(ArgumentNullException))]
        public void throws_when_url_is_empty_string()
        {
            this.writer.Href(string.Empty, "Some title");
        }

        [Test, ExpectedException(typeof(ArgumentNullException))]
        public void throws_when_title_is_null()
        {
            this.writer.Href("http://foo.com/", null);
        }

        [Test, ExpectedException(typeof(ArgumentNullException))]
        public void throws_when_title_is_empty_string()
        {
            this.writer.Href("http://foo.com/", string.Empty);
        }
    }
}
